/**
 * 
 */
package br.unifor.mia.cd.middleware.util;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.apache.commons.beanutils.BeanUtils;

import arcademis.MarshalException;
import arcademis.Stream;
import br.unifor.mia.cd.middleware.encrypt.algorithm.Cryptor;
import br.unifor.mia.cd.middleware.encrypt.annotation.Encryptable;
import br.unifor.mia.cd.middleware.encrypt.marshalable.Marshalable;

/**
 * @author arthurlandim
 *
 */
public class MarshalableUtil {
	
	/**
	 * Retornar uma lista de metodos get da classe ordenada pelo nome do atributo
	 * @param obj
	 * @return lista de atributos
	 */
	public static List<Method> sortListField(Object obj) {
		Method[] methodArray = obj.getClass().getDeclaredMethods();
		List<Method> fieldList = new ArrayList<Method>();
		for (Method method : methodArray) {
			if (method.isAnnotationPresent(Marshalable.class)
					&& method.getName().startsWith("get")) {
				fieldList.add(method);
			}
		}
		Collections.sort(fieldList, new MethodComparator());
		return fieldList;
	}
	
	/**
	 * Pegar o valor de um atributo de um campo.
	 * Uso o BeanUtils. Serve apenas para encapsular as exceptions que podem ser lançadas
	 * @param bean
	 * @param fieldName
	 * @return valor do atributo
	 */
	public static Object getProperty(Object bean, String fieldName) {
		Object result = null;
		try {
			result =  BeanUtils.getProperty(bean, fieldName);
			
		} catch (Exception e) {
			e.printStackTrace();
		}
		return result;
	}
	
	/**
	 * Pegar o valor de um atributo de um campo.
	 * Uso o BeanUtils. Serve apenas para encapsular as exceptions que podem ser lançadas
	 * @param bean
	 * @param fieldName
	 * @return valor do atributo
	 */
	public static Object getPropertyFromGetMethod(Object bean, Method method) {
		Object result = null;
		try {
			result = method.invoke(bean, new Object[0]);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return result;
	}
	
	/**
	 * Pegar o valor de um atributo de um campo.
	 * Uso o BeanUtils. Serve apenas para encapsular as exceptions que podem ser lançadas
	 * @param bean
	 * @param fieldName
	 */
	public static void setPropertyThroughMethod(Object bean, Method method, Object value) {
		try {
			Method setMethod = null;
			Class clazz = method.getDeclaringClass();
			String setMethodName = method.getName().replaceFirst("get", "set");
			setMethod = clazz.getMethod(setMethodName, new Class[]{value.getClass()});
			setMethod.invoke(bean, new Object[]{value});
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * (Un)Marshal a object in/from a stream
	 * @param stream
	 * @param marshal if true: to marshal, if false: to unmarshal
	 * @param obj Objecto to (un)marshal
	 * @throws MarshalException
	 */
	public static void marshal(Stream stream, Object obj) throws MarshalException {
		marshaling(stream, true, obj);
	}	
	/**
	 * (Un)Marshal a object in/from a stream
	 * @param stream
	 * @param marshal if true: to marshal, if false: to unmarshal
	 * @param obj Objecto to (un)marshal
	 * @throws MarshalException
	 */
	public static void unmarshal(Stream stream, Object obj) throws MarshalException {
		marshaling(stream, false, obj);
	}	
	/**
	 * (Un)Marshal a object in/from a stream
	 * @param stream
	 * @param marshal if true: to marshal, if false: to unmarshal
	 * @param obj Objecto to (un)marshal
	 * @throws MarshalException
	 */
	private static void marshaling(Stream stream, Boolean marshal, Object obj) 
																		throws MarshalException {
		//Pegando lista de atributos ordenada pelo nome
		List<Method> fieldList = MarshalableUtil.sortListField(obj);
		
		/*
		 * Verificando se a classe possui a anotação de criptografia:
		 * caso verdadeiro, todos os atributos vão ser criptografados
		 * caso falso, apenas os atributos com a anotação o serão  
		 */
		boolean allFieldsEncryptable = false;
		Cryptor cryptor = null;
		if (obj.getClass().isAnnotationPresent(Encryptable.class)) {
			//Pegando a classe especificada na anotação
			cryptor = obj.getClass().getAnnotation(Encryptable.class).value().getCryptor();
			allFieldsEncryptable = true;
		}
		for (Method methodGet : fieldList) {
			Object value = null;
			if (marshal) {
				//Se for para marshal
				value = getPropertyFromGetMethod(obj, methodGet);
			} else {
				//Se for para unmarshal
				value = stream.readObject();
			}
			if (allFieldsEncryptable || methodGet.isAnnotationPresent(Encryptable.class)) {
				//Caso a classe tenha a anotação ou o atributo tenha uma, então deve-se criptografar
				
				Cryptor fieldCryptor = (methodGet.isAnnotationPresent(Encryptable.class) ? 
							methodGet.getAnnotation(Encryptable.class).value().getCryptor() : null );
				//Verificando se o atributo possui a anotação de criptografia

				/*
				 * Escolher o cryptor a ser usado. 
				 * O atributo poderá ter especificado um cryptor específico
				 */
				fieldCryptor = (fieldCryptor != null ? fieldCryptor : cryptor);
				
				//Se for para marshal
				value = (marshal ? fieldCryptor.encrypt(value) : fieldCryptor.decrypt(value));
			}
			
			if (marshal) {
				//Se for para marshal
				stream.write(value.toString());
			} else {
				//Se for para unmarshal
				setPropertyThroughMethod(obj, methodGet, value);
			}
		}	
	}
	
}
